package sw6.admin.parser;

import java.io.IOException;
import java.io.InputStream;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

import sw6.lib.internal.PrivateDefinitions;
import sw6.admin.internal.Definitions;

import android.content.Context;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.res.AssetManager;
import android.content.res.Resources;
import android.util.Log;

/**
 * 
 * @author sw6b
 */
public class SettingsParser {
	private static final String LOG_NAME = "sw6.admin.parser.SettingsParser";
	
	/**
	 * Find and parse settings.xml file for a given application given 
	 * by appName and use the given strategy to handle different
	 * types of elements (eg. integer, double, enum).
	 * @param context Any given context.
	 * @param appName Canonical name of the app to parse settings for, e.g.: <code>sw6.moduleName.appName</code>.
	 * @param injection An object that implements ISettingsParserStrategy.
	 * @throws NameNotFoundException if appname is not recognized by packageManager.
	 * @throws IOException is returned if something bad happen with the IO.
	 */
	public static void parse(Context context, String appName, ISettingsParserInjection injection) throws NameNotFoundException, IOException {
		Log.i(LOG_NAME, "Calling: " + injection.getClass().getCanonicalName() + ".init(" + context.toString() + ", " + appName + ").");
		injection.init(context, appName);
		
		if (SettingsParser.doWeHaveSettingsFile(context, appName)) {
			Log.i(LOG_NAME, appName + " does contain a setting.xml file and we store the settings in the database.");
			
			// Get asset manager for application
			AssetManager am = getAssetsByAppName(context, appName);

			// Setup the XML DOM parser
			DocumentBuilderFactory dbf 	= DocumentBuilderFactory.newInstance();
			DocumentBuilder docBuilder	= null;
			try {
				docBuilder = dbf.newDocumentBuilder();
			} catch (ParserConfigurationException e) {
				e.printStackTrace();
			}

			// Define inputstream and a HTML / XML document
			InputStream inputStream = null;
			Document dom 			= null;

			// Open settings XML file from the assets folder in another app
			try {
				inputStream = am.open(PrivateDefinitions.SETTINGS_FILE_NAME);
				dom 		= docBuilder.parse(inputStream);
			} catch (SAXException e) {
				e.printStackTrace();
			} catch (IOException e) {
				e.printStackTrace();
			}

			/* * * * *
			 * Setup is now done. The XML file is loaded from another app, 
			 * and the we have a DOM representation of the XML file.
			 * Next is to parse the XML file and store settings in the db. */
			Element rootElement 		  = dom.getDocumentElement();
			NodeList intSettings 		  = rootElement.getElementsByTagName(Definitions.XML_SETTING_TYPE_INT);
			NodeList doubleSettings		  = rootElement.getElementsByTagName(Definitions.XML_SETTING_TYPE_DOUBLE);
			NodeList booleanSettings 	  = rootElement.getElementsByTagName(Definitions.XML_SETTING_TYPE_BOOLEAN);
			NodeList stringSettings 	  = rootElement.getElementsByTagName(Definitions.XML_SETTING_TYPE_STRING);
			NodeList objectSettings  	  = rootElement.getElementsByTagName(Definitions.XML_SETTING_TYPE_OBJECT);
			NodeList stdobjectSettings    = rootElement.getElementsByTagName(Definitions.XML_SETTING_TYPE_STDOBJECT);
			NodeList enumSettings  	  	  = rootElement.getElementsByTagName(Definitions.XML_SETTING_TYPE_ENUM);

			for(int i = 0; i < intSettings.getLength(); i++) {
				Element setting 	= (Element) intSettings.item(i);
				String varName		= setting.getAttribute(Definitions.XML_ATTRIBUTE_VAR_NAME);
				int varValue 		= PrivateDefinitions.DEFAULT_INTEGER;
				int min 			= Integer.MIN_VALUE;
				int max				= Integer.MAX_VALUE;

				String minTemp = setting.getAttribute(Definitions.XML_ATTRIBUTE_MIN);
				if (minTemp.length() > 0) {
					min = Integer.parseInt(minTemp);
				}

				String maxTemp = setting.getAttribute(Definitions.XML_ATTRIBUTE_MAX);
				if (maxTemp.length() > 0) {
					max = Integer.parseInt(maxTemp);
				}

				Node selectedNode = setting.getChildNodes().item(0);
				if (selectedNode != null) {
					/**
					 * If there are given a default value we return that. Because
					 * of our early XML check we know the default value is between 
					 * minimum and maximum value.
					 */
					varValue = Integer.parseInt(selectedNode.getNodeValue());
				} else {
					if (max >= PrivateDefinitions.DEFAULT_INTEGER && min <= PrivateDefinitions.DEFAULT_INTEGER) {
						varValue = PrivateDefinitions.DEFAULT_INTEGER;
					} else {
						varValue = (Math.abs(min) < Math.abs(max)) ? min : max;
					}
				}

				Log.i(LOG_NAME, "Calling: " + injection.getClass().getCanonicalName() + ".handleInteger(" + varName + ", " + min + ", " + max + ", " + varValue + ").");
				injection.handleInteger(varName, min, max, varValue);
			}

			for(int i = 0; i < doubleSettings.getLength(); i++) {
				Element setting 	= (Element) doubleSettings.item(i);
				String varName 		= setting.getAttribute(Definitions.XML_ATTRIBUTE_VAR_NAME);
				double varValue 	= PrivateDefinitions.DEFAULT_DOUBLE;
				double min 			= -Double.MAX_VALUE;
				double max			= Double.MAX_VALUE;

				String minTemp = setting.getAttribute(Definitions.XML_ATTRIBUTE_MIN);
				if (minTemp.length() > 0) {
					min = Double.parseDouble(minTemp);
				}

				String maxTemp = setting.getAttribute(Definitions.XML_ATTRIBUTE_MAX);
				if (maxTemp.length() > 0) {
					max = Double.parseDouble(maxTemp);
				}

				Node selectedNode = setting.getChildNodes().item(0);
				if (selectedNode != null) {
					/**
					 * If there are given a default value we return that. Because
					 * of our early XML check we know the default value is between 
					 * minimum and maximum value.
					 */
					varValue = Double.parseDouble(selectedNode.getNodeValue());
				} else {
					if (max >= PrivateDefinitions.DEFAULT_DOUBLE && min <= PrivateDefinitions.DEFAULT_DOUBLE) {
						varValue = PrivateDefinitions.DEFAULT_DOUBLE;
					} else {
						varValue = (Math.abs(min) < Math.abs(max)) ? min : max;
					}
				}

				Log.i(LOG_NAME, "Calling: " + injection.getClass().getCanonicalName() + ".handleDouble(" + varName + ", " + min + ", " + max + ", " + varValue + ").");
				injection.handleDouble(varName, min, max, varValue);
			}

			for(int i = 0; i < booleanSettings.getLength(); i++) {
				Element setting 	= (Element) booleanSettings.item(i);
				String varName 		= setting.getAttribute(Definitions.XML_ATTRIBUTE_VAR_NAME);
				boolean varValue 	= PrivateDefinitions.DEFAULT_BOOLEAN;

				Node selectedNode = setting.getChildNodes().item(0);
				if (selectedNode != null) {
					/**
					 * If there are given a default value we return that. Because
					 * of our early XML check we know the default value is "true"
					 * or "false" and nothing else.
					 */
					varValue = (selectedNode.getNodeValue().equals("true")) ? true : false;
				}

				Log.i(LOG_NAME, "Calling: " + injection.getClass().getCanonicalName() + ".handleBoolean(" + varName + ", " + varValue + ").");
				injection.handleBoolean(varName, varValue);
			}

			for(int i = 0; i < stringSettings.getLength(); i++) {
				Element setting 	= (Element) stringSettings.item(i);
				String varName 		= setting.getAttribute(Definitions.XML_ATTRIBUTE_VAR_NAME);
				String varValue 	= PrivateDefinitions.DEFAULT_STRING;
				int min 			= 0; // A string length can't be negative
				int max				= Integer.MAX_VALUE;

				String minTemp = setting.getAttribute(Definitions.XML_ATTRIBUTE_MIN);
				if (minTemp.length() > 0) {
					min = Integer.parseInt(minTemp);
				}

				String maxTemp = setting.getAttribute(Definitions.XML_ATTRIBUTE_MAX);
				if (maxTemp.length() > 0) {
					max = Integer.parseInt(maxTemp);
				}

				Node selectedNode = setting.getChildNodes().item(0);
				if (selectedNode != null) {
					/**
					 * If there are given a default value we return that. Because
					 * of our early XML check we know the default value is between 
					 * minimum and maximum value.
					 */
					varValue = selectedNode.getNodeValue();
				} else {
					int stringLength;
					StringBuilder sb = new StringBuilder();

					if (max >= PrivateDefinitions.MIN_STRING_LENGTH && min <= PrivateDefinitions.MIN_STRING_LENGTH) {
						stringLength = PrivateDefinitions.MIN_STRING_LENGTH;
					} else {
						stringLength = (Math.abs(min) < Math.abs(max)) ? min : max;
					}

					for (int k = 0; k < stringLength; k++) {
						sb.append(' ');
					}

					varValue = sb.toString();
				}

				Log.i(LOG_NAME, "Calling: " + injection.getClass().getCanonicalName() + ".handleString(" + varName + ", " + min + ", " + max + ", " + varValue + ").");
				injection.handleString(varName, min, max, varValue);
			}

			for(int i = 0; i < objectSettings.getLength(); i++) {
				Element setting 	= (Element) objectSettings.item(i);
				String varName 		= setting.getAttribute(Definitions.XML_ATTRIBUTE_VAR_NAME);
				String type 		= setting.getAttribute(Definitions.XML_ATTRIBUTE_TYPE_NAME);
				
				Log.i(LOG_NAME, "Calling: " + injection.getClass().getCanonicalName() + ".handleObject(" + varName + ", " + type + ", " + PrivateDefinitions.DEFAULT_OBJECT + ").");
				injection.handleObject(varName, type, PrivateDefinitions.DEFAULT_OBJECT);
			}
			
			/**
			 *  We do the same with stdobjects as objects and call the same 
			 *  strategy method as we do on objects. The reason is we already
			 *  save the objects and stdobjects in the same table in the
			 *  database.
			 */
			for(int i = 0; i < stdobjectSettings.getLength(); i++) {
				Element setting 	= (Element) stdobjectSettings.item(i);
				String varName 		= setting.getAttribute(Definitions.XML_ATTRIBUTE_VAR_NAME);
				String type 		= setting.getAttribute(Definitions.XML_ATTRIBUTE_TYPE_NAME);
				
				Log.i(LOG_NAME, "Calling: " + injection.getClass().getCanonicalName() + ".handleObject(" + varName + ", " + type + ", " + PrivateDefinitions.DEFAULT_OBJECT + ").");
				injection.handleObject(varName, type, PrivateDefinitions.DEFAULT_OBJECT);
			}

			for(int i = 0; i < enumSettings.getLength(); i++) {
				Element setting 	= (Element) enumSettings.item(i);
				String varName		= setting.getAttribute(Definitions.XML_ATTRIBUTE_VAR_NAME);
				NodeList enumElements = setting.getElementsByTagName(Definitions.XML_SETTING_TYPE_ENUM_ELEMENT);

				for(int k = 0; k < enumElements.getLength(); k++) {
					Element enumElement	= (Element) enumElements.item(k);
					int varValue		= Integer.parseInt(enumElement.getAttribute(Definitions.XML_ATTRIBUTE_VALUE));
					
					Log.i(LOG_NAME, "Calling: " + injection.getClass().getCanonicalName() + ".handleEnumElement(" + varName + ", " + varValue + ").");
					injection.handleEnumElement(varName, varValue);
				}

				// We take the first enum element as the default value
				int varValue = Integer.parseInt(((Element)enumElements.item(0)).getAttribute(Definitions.XML_ATTRIBUTE_VALUE));
				Log.i(LOG_NAME, "Calling: " + injection.getClass().getCanonicalName() + ".handleEnum(" + varName + ", " + varValue + ").");
				injection.handleEnum(varName, varValue);
			}
		} else {
			Log.i(LOG_NAME, appName + " does not contain a setting.xml file.");
		}
		
		Log.i(LOG_NAME, "Calling: " + injection.getClass().getCanonicalName() + ".end().");
		injection.end();
	}
	
	public static AssetManager getAssetsByAppName(Context context, String appName) throws NameNotFoundException {
		// Get instance of package manager
		PackageManager pm 	= context.getPackageManager();
		Resources resc 		= null;
		
		resc = pm.getResourcesForApplication(appName);

		return resc.getAssets();
	}
	
	public static boolean doWeHaveSettingsFile(Context context, String packageName) throws IOException, NameNotFoundException {
		try {
			// Try open the file and close it right after. This check if the file exist
			getAssetsByAppName(context, packageName).open(PrivateDefinitions.SETTINGS_FILE_NAME).close();
			return true;
		} catch (IOException e) {
			// If the file does not exist (or unavailable) we will get an exception
			return false;
		}
		
		// It's faster to try open the specific file and use the IOException to determine the existence of the file
//		for (String fileName : getAssetsByAppName(context, packageName).list("")) {
//			if (fileName.equals(PrivateDefinitions.SETTINGS_FILE_NAME)) {
//				return true;
//			}
//		}
//		return false;
	}
}
